// Copyright (c) 2010 Manuel Peinado Gallego <manuel.peinado@gmail.com>
// Distributed under the MIT license

#include "stdafx.h"
#include "View3d.h"
#include "Drawable.h"
#include "Dim.h"
#include "Ui.h"

bool DrawableOrderComparer::operator()(Drawable* a, 
                                       Drawable* b) const
{
    int sign = a->getDrawOrder() - b->getDrawOrder();
    if(sign < 0)
    {
        return true;
    }
    else if(sign > 0)
    {
        return false;
    }
    else
    {
        return a < b; // ptr comparison
    }
}

bool DrawableOrderComparer::operator()(ref<Drawable> a, 
                                       ref<Drawable> b) const
{
    return operator()(a.get(), b.get());
}

// -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  


View3d::View3d(int WorldFollower, int uiViewFlags)
: WorldFollower(WorldFollower), UiFollower(uiViewFlags),
  m_drawingInPickMode(false), m_hoverItem(NULL),
  m_fov(PI * .25), m_projectionMode(ProjectionModes::Orthographic),
  m_orthoScale(1.), m_backgroundColor(.25f, .15f, .25f),
  m_backgroundGradient(true), m_drawing(false),
  m_middleBtnDown(false), m_cameraMat(gx::eye)
{
    m_cameraMat.make_transl(0, 1., 2.);

}

// -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  

View3d::~View3d()
{
}

// -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  

void View3d::onSelectionChanged(SelectionManager* /*sender*/)
{
    invalidate();
}
    
void View3d::onNodeEvent(const NodeEvent&)
{
    invalidate();
}

void View3d::onMaterialEvent(const MaterialEvent&)
{
    invalidate();
}

void View3d::onShaderEvent(const ShaderEvent&)
{
    invalidate();
}

// -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  

bool View3d::isDrawingInPickMode() const 
{
    return m_drawingInPickMode;
}

void View3d::addDrawable(ref<Drawable> drawable) 
{
    m_drawables.insert(drawable);
    invalidate();
}

void View3d::addPickable(ref<Pickable> pickable) 
{
    addDrawable(pickable);
    PickColor pickColor = findFirstUnusedPickColor();
    pickable->setPickColor(pickColor);
    m_pickableColors.insert(std::make_pair(pickColor, pickable.get()));
    m_pickables.insert(pickable.get());
}

void View3d::removeDrawable(ref<Drawable> drawable) 
{
    ASSERT(m_drawables.find(drawable) != m_drawables.end());
    m_drawables.erase(drawable);
}

void View3d::removePickable(ref<Pickable> pickable) 
{
    removeDrawable(pickable);
    ASSERT(m_pickables.find(pickable.get()) != m_pickables.end());

    NameToPickableMap::iterator colorIt =
        m_pickableColors.find(pickable->getPickColor());
    ASSERT(colorIt != m_pickableColors.end());
    
    m_pickables.erase(pickable.get());
    m_pickableColors.erase(colorIt);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Pickable* View3d::getHoverItem() const
{
    return m_hoverItem;
}

int View3d::getHoverItemPart() const
{
    return m_hoverItemPart;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    
gx::Mat4 View3d::getCameraMatrix() const
{
    return getCameraMatrixImpl();
}

gx::Vec3 View3d::unproject(const ScreenPoint & p, double z) const
{
    return unprojectImpl(p, z);
}

ScreenPoint View3d::project(const gx::Vec3 & v) const
{
    return projectImpl(v);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

void View3d::setProjectionMatrix()
{
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();

    double aspect = getAspect();

    if(m_projectionMode == ProjectionModes::Perspective)
    {
        gluPerspective(radToDeg(m_fov), aspect, 0.1, 1000.);
    }
    else
    {
        ASSERT(m_projectionMode == ProjectionModes::Orthographic);
        double zoom = m_orthoScale;

        int w, h;
        getDims(w, h);
        if(w > h)
        {
            glOrtho(-zoom * aspect, zoom * aspect, -zoom , zoom, -100., 100.);
        }
        else
        {
            glOrtho(-zoom, zoom, -zoom / aspect, zoom / aspect, -100., 100.);
        }
	}
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

Ray View3d::computePickRay(const ScreenPoint & p) const
{
    // Note: in perspective mode it's faster to just do "unproj1 = camera.pos"

    gx::Vec3 unproj0 = unproject(p, 0);
    gx::Vec3 unproj1 = unproject(p, 1.);
    return Ray(unproj0, (unproj1 - unproj0).normalize());
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

void View3d::setFov(double angle)
{
    ASSERT(angle >= 0. && angle <= PI);
    ASSERT(m_projectionMode == ProjectionModes::Perspective);
    m_fov = angle;
}

double View3d::getFov() const
{
    ASSERT(m_projectionMode == ProjectionModes::Perspective);
    return m_fov;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

ProjectionMode View3d::getProjectionMode() const
{
    return m_projectionMode;
}

void View3d::setProjectionMode(ProjectionMode mode)
{
    ASSERT(mode == ProjectionModes::Perspective 
        || mode == ProjectionModes::Orthographic);
    m_projectionMode = mode;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

void View3d::getDims(int & w, int & h) const
{
    return getDimsImpl(w, h);
}

void View3d::invalidate()
{
    if(m_drawing)
    {
        return;
    }
    invalidateImpl();
}

PickColor View3d::findFirstUnusedPickColor() const
{
    PickColor color(0, 0);
    PickColor end;
    for(; color < end; ++color)
    {
        if(m_pickableColors.find(color) 
                == m_pickableColors.end())
        {
            break;
        }
    }
    return color;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Pickable* View3d::doPickTest(const ScreenPoint & point_,
                             int & subName)
{
    // TODO No idea why this is necessary
    ScreenPoint point = point_;
    //point.y += 5;

    m_drawingInPickMode = true;

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    setProjectionMatrix();
    
    // Finally get back to the modelview matrix mode, and initialise 
    // the Name Stack to start rendering.
    setModelViewMatrix();

    glPushAttrib(GL_CURRENT_BIT | GL_ENABLE_BIT);
    glDisable(GL_DITHER);
    glDisable(GL_LIGHTING);
    glDisable(GL_FOG);
    
    NameToPickableMap::iterator pickableIt = m_pickableColors.begin();
    for(; pickableIt != m_pickableColors.end(); ++pickableIt)
    {
        Pickable* pickable = pickableIt->second;
        ASSERT(pickable->hasPickColor());
        ASSERT(pickable->getPickColor() == pickableIt->first);
        pickable->getPickColor().setInRenderContext();

        Transformable* transformable = dynamic_cast<Transformable*>(pickable);
        if(transformable)
        {
            glPushMatrix();
            gx::Mat4 mat = transformable->getGlobalTransform();
            double m[16];
            glMultMatrixd(mat.to_array(m));
            
            pickable->draw(this);
        
            glPopMatrix();
        }
        else
        {
            pickable->draw(this);
        }
    }
    glPopAttrib();

    Pickable* result = NULL;
    subName = -1;
    result = processPick(point, subName);

    if(result != NULL)
    {
        result->getPickColor();
    }

    m_drawingInPickMode = false;
    return result;
}

Pickable* View3d::processPick(const ScreenPoint & point, int & subName)
{
    GLint viewport[4];
    glGetIntegerv(GL_VIEWPORT,viewport);

    GLubyte pixel[3];
    glReadPixels(point.x, viewport[3] - point.y, 1, 1,
                 GL_RGB,GL_UNSIGNED_BYTE, (void *)pixel);
    
    NameToPickableMap::iterator it = m_pickableColors.find(PickColor(pixel));
    if(it != m_pickableColors.end())
    {
        subName = pixel[2];
        return it->second;
    }
    else 
    {
        subName = -1;
        return NULL;
    }
}


void View3d::setModelViewMatrix()
{
    glMatrixMode(GL_MODELVIEW);

    gx::Mat4 inv = gx::Mat4(getCameraMatrix()).invert_affine();
    double m[16];
    glLoadMatrixd(inv.to_array(m));
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

double View3d::getAspect() const
{
    int w, h;
    getDims(w, h);
    return w / double(h);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

double View3d::getOrthoScale() const
{
    ASSERT(m_projectionMode == ProjectionModes::Orthographic);
    return m_orthoScale;
}


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

double View3d::computeConstantScaleFactor(
    const gx::Vec3& objPos) const
{
    // Make the gizmo size depend on the vertical size of the screen.
    bool perspective = m_projectionMode == ProjectionModes::Perspective;
    double screenVertSize;
    double sizeFactor;
    if(perspective)
    {
        gx::Mat4 cameraMatInv = getCameraMatrix().invert();
        gx::Vec3 frameCenterInEye = cameraMatInv * gx::Pnt(objPos);
        sizeFactor = fabs(frameCenterInEye.z);
        screenVertSize = .5 * std::tan(.5 * m_fov); 
    }
    else
    {
        sizeFactor = m_orthoScale;
        screenVertSize = 1.;
    }
    return screenVertSize * sizeFactor;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

void View3d::drawBackground()
{
    if(!m_backgroundGradient)
    {
        glClear(GL_COLOR_BUFFER_BIT);
        return;
    }

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(-1, 1, -1, 1, -10, 10);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glPushAttrib (GL_POLYGON_BIT | GL_LIGHTING_BIT | GL_DEPTH_BUFFER_BIT);
        
        glShadeModel(GL_SMOOTH);
        glPolygonMode(GL_FRONT, GL_FILL);
        glDisable(GL_LIGHTING);
        glDisable(GL_DEPTH_TEST); 
 
        Color colorUp = m_backgroundColor;
        Color colorDown = makeBrighter(colorUp, 3);

        // draw the quad in front of far plane
        glBegin(GL_QUADS);
            colorUp();
            glVertex3f( -1, 1, 0);

            colorDown();
            glVertex3f( -1, -1, 0);

            colorDown();
            glVertex3f( 1, -1, 0);

            colorUp();
            glVertex3f( 1, 1, 0);
        glEnd(); 
    glPopAttrib();
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

void View3d::addRenderListener(RenderListener* listener)
{
    m_renderListeners.insert(listener);
}

void View3d::beginRender()
{
    // We use this to prevent entering an infinite notification loop
    m_drawing = true;

    std::set<RenderListener*>::iterator it = m_renderListeners.begin();
    for(; it != m_renderListeners.end(); ++it)
    {
        RenderListener* listener = *it;
        listener->onBeginRender(this);
    }
}
 
void View3d::endRender()
{
    std::set<RenderListener*>::iterator it = m_renderListeners.begin();
    for(; it != m_renderListeners.end(); ++it)
    {
        RenderListener* listener = *it;
        listener->onEndRender(this);
    }

    m_drawing = false;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

void View3d::onCameraDrag(CPoint iniPoint, int currFlags, CPoint currPoint)
{
    int w, h;
    getDims(w, h);

    double dX = (currPoint.x - iniPoint.x) / (double)w;
    double dY = (currPoint.y - iniPoint.y) / (double)h;

    m_cameraMat = m_iniCam;

    // Middle button + shift => shift camera in the screen plane
    if((currFlags & MK_SHIFT) == MK_SHIFT)
    {
        cameraShift(5. * dX, -5. * dY);
    }
    else
    {
        // Yaw is rotation about screen vertical axis
        gx::Mat3 yawMat;
        yawMat.make_rot(gx::AxisAngle(-m_cameraMat.get_col(Dims::Y), dX));

        // Pitch is rotation about screen horizontal axis
        gx::Mat3 pitchMat;
        pitchMat.make_rot(gx::AxisAngle(-m_cameraMat.get_col(Dims::X), dY));
        
        // For n>2 product of rotation matrices is not commutative, but here
        // it doesn't seem to matter much (maybe because we have small rotations,
        // and the product of infinitesimal rotations commutes). Here we tilt and then 
        // swing but it could have been the other way around
        gx::Mat3 pitchYawMat = yawMat * pitchMat;

        // Middle button + ctrl => rotate camera about selected 
        // object (or about the origin), like 3D editors (Blender)
        if((currFlags & MK_CONTROL) == MK_CONTROL)
        {
            gx::Vec3 pivot = gx::zero;
            Selectable* selection = 
                getUi().getSelectionManager().getSelection();
            
            if(TransformNode* selNode = dynamic_cast<TransformNode*>(selection))
            {
                pivot = selNode->getGlobalTransform().translation;
            }

            // This is the way to rotate an object X about a pivot point P:
            // X = [T(P) * R * T(-P)] * X;
            m_cameraMat = gx::Mat4().make_pivot_rot(pivot, pitchYawMat) 
                         * m_cameraMat;
        }

        // Middle button only => camera tilt and swing (like a FPS)
        else
        {
            // TODO exctract this into a method like cameraDolly or shift

            // Intrinsic rotation (about object's own origin)
            // This is the way to do it using pivot rotation (M is the camera matrix)
            // M = (gx::Mat4(M.translation) * swingTiltMat * gx::Mat4(-M.translation)) * M;
            // But [gx::Mat4(-M.translation)) * M] equals M.rotation, and 
            // [gx::Mat4(M.translation) * swingTiltMat * M.rotation] equals 
            // gx::Mat4(swingTiltMat * M.rotation, M.translation)], so we can simply write:
            
            m_cameraMat.rotation = pitchYawMat * m_cameraMat.rotation;
        }
        invalidate();
    }
    // TODO maybe make this shift + rght button
    // Camera cameraDolly
    //else if(m_middleBtnDown)
    //{
    // m_cameraMat.translation = m_iniCamPos;
    // cameraDolly(5. * dY);
    //}

}



void View3d::cameraShift(double dX, double dY)
{
    gx::Vec3 offset = m_cameraMat * gx::Dir(gx::I * dX + gx::J * dY);
    m_cameraMat.translation += offset;
    invalidate();
}

void View3d::cameraDolly(double amount)
{
    if(m_projectionMode == ProjectionModes::Perspective)
    {
        gx::Vec3 offset = m_cameraMat * gx::Dir(gx::K * amount);
        m_cameraMat.translation += offset;
    }
    else
    {
        // In ortho mode we can't use eyeZ as a measure of Zoom because
        // then camera rotations cause annoying zoom in and outs
        m_orthoScale *= (1. + amount * .3);
    }
    invalidate();
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

void View3d::addContinuousRenderRequester(ContinuousRenderRequester* req)
{
    m_continuousRenderRequesters.insert(req);
}


void View3d::onIdle(double elapsedMicroSecs)
{
    bool ctrlDown = ::GetKeyState(VK_CONTROL) < 0;

    if(isKeyDown(VK_LEFT) || isKeyDown(VK_RIGHT))
    {
        if(ctrlDown)
        {
            // radians per microsecond
            const double YAW_SPEED_US = TWO_PI * SECS_IN_US / 2.; 
            double yawDelta = YAW_SPEED_US * elapsedMicroSecs;
            yawDelta = isKeyDown(VK_LEFT) ? yawDelta : -yawDelta;

            gx::Vec3 camY = m_cameraMat.get_col(Dims::Y);
            gx::Mat3 yawMat = gx::Mat3().make_rot(gx::AxisAngle(camY, yawDelta));
            m_cameraMat.rotation = yawMat * m_cameraMat.rotation;
            invalidate();
        }
        else
        {
            if(isKeyDown(VK_LEFT))
            {
                cameraShift(-STEPPING_SPEED * elapsedMicroSecs, 0.);
            }
            else if(isKeyDown(VK_RIGHT))
            {
                cameraShift(STEPPING_SPEED * elapsedMicroSecs, 0.); 
            }
        }
    }

    if(isKeyDown(VK_UP) || isKeyDown(VK_DOWN)) 
    {
        if(ctrlDown)
        {
            const double PITCH_SPEED_US = TWO_PI * SECS_IN_US / 2.; 
            double pitchDelta = PITCH_SPEED_US * elapsedMicroSecs;
            pitchDelta = isKeyDown(VK_UP) ? pitchDelta : -pitchDelta;

            gx::Vec3 camX = m_cameraMat.get_col(Dims::X);
            gx::Mat3 pitchMat = gx::Mat3().make_rot(gx::AxisAngle(camX, pitchDelta));
            m_cameraMat.rotation = pitchMat * m_cameraMat.rotation;

            invalidate();
        }
        else
        {
            if(isKeyDown(VK_UP))
            {
                cameraDolly(-STEPPING_SPEED * elapsedMicroSecs);
            }
            else if(isKeyDown(VK_DOWN))
            {
                cameraDolly(STEPPING_SPEED * elapsedMicroSecs);
            }
        }
    }

    if(isKeyDown(VK_PRIOR) || isKeyDown(VK_NEXT))
    {
        if(ctrlDown)
        {
            // radians per microsecond
            const double ROLL_SPEED_US = TWO_PI * SECS_IN_US / 2.; 
            double rollDelta = ROLL_SPEED_US * elapsedMicroSecs;
            rollDelta = isKeyDown(VK_PRIOR) ? rollDelta : -rollDelta;

            gx::Vec3 camZ = m_cameraMat.get_col(Dims::Z);
            gx::Mat3 rollMat = gx::Mat3().make_rot(gx::AxisAngle(camZ, rollDelta));
            m_cameraMat.rotation = rollMat * m_cameraMat.rotation;
            invalidate();
        }
        else
        {
            if(isKeyDown(VK_PRIOR))
            {
                cameraShift(0., STEPPING_SPEED * elapsedMicroSecs);
            }
            else if(isKeyDown(VK_NEXT))
            {
                cameraShift(0., -STEPPING_SPEED * elapsedMicroSecs);
            }
        }
    }

    typedef std::set<ContinuousRenderRequester*>::iterator It;
    for(It it = m_continuousRenderRequesters.begin(); 
        it != m_continuousRenderRequesters.end(); ++it)
    {
        if((*it)->needsContinuousRender(this))
        {
            invalidate();
            break;
        }
    }

    //if(AnimationManager::instance().performAnimationStep(elapsedMicroSecs / 1000.))
    //{
    // invalidate();
    //}
}
